LINE Bot開発におけるオニオンアーキテクチャ
はじめに
こんばんは,小栗研究室B4の西澤悠貴です.本記事では,研究でLINE Botを開発した際にオニオンアーキテクチャを採用したため,それについて解説していけたらなと思います.ただ,私はアーキテクチャや設計に関して造詣が深いわけではないので,現状の実装や理解が十全とは言えません.もし間違ってたら指摘いただけると幸いです.また,本記事では設計やアーキテクチャを主題とした記事であるため,LINE Bot開発上の専門用語はあんまり解説しません. 動作環境
Typescript - 4.9.0
express - 4.18.2
firebase - 9.21.0
firebase-admin - 11.5.0
firebase-functions - 4.2.0
reflect-metadata - 0.1.13
tsyringe - 4.8.0
オニオンアーキテクチャとは
オニオンアーキテクチャは,図1のように階層構造が同心円状に表現されることが多いです.オニオン(たまねぎ)の由来はこの図から来ています.
table:図1 オニオンアーキテクチャの同心円状階層構造(mermaidで同心円状が表現できませんでした)
https://gyazo.com/62635252f6edd708574d18c0aacf461d
各レイヤーは自身の内側のレイヤーにのみ依存します.依存関係の矢印が常に内側に向かっていくようなイメージです.一方で内側から外側に依存することはありません.そうすることでレイヤーごとに関心ごとを分離することができ,従来のレイヤードアーキテクチャよりも結合度を下げることができます.
レイヤードアーキテクチャは原始的なアーキテクチャで図2のようにただ上の層が下の層にのみ依存するという単純なものです.
code:図2 レイヤードアーキテクチャの階層構造.mermaid
graph TD
UI --> App
App --> domain
domain --> infra
他のアーキテクチャとして,ヘキサゴナルアーキテクチャやクリーンアーキテクチャなどを聞いたことのある方も多いと思います.調べると概念図が色々と出てくると思いますが,これらは本質的にオニオンアーキテクチャと同等のものだと私は認識しています.レイヤーごとに責務を適切に分割することで,結合を疎にし凝縮を密にし,大規模な開発でも高い拡張性と保守性を保つことを実現しています(他のアーキテクチャでも同様ですが).同じ責務を持ったレイヤーの名前がそれぞれのアーキテクチャごとに違うという感じです.
それと注意点ですが,システムのアーキテクチャに「これ1つで全て完璧!」というものは存在しません.クリーンアーキテクチャ然りMVVM然りFlux然り,アーキテクチャごとに目的も強みも異なります.そのため,不適切なアーキテクチャを選択してしまうとメリットをデメリットが上回ってしまいいいことがありません.ちなみに自分はアーキテクチャを用いた開発の経験が片手の指にも足りないため,実際にそれは経験したことがありません.ただ,秩序も利点もないアーキテクチャでは荒廃することは容易く予想はつくと思います.
LINE Bot開発
開発したLINE Botは図3のようなディレクトリ構造になっています.
code:図3 LINE Botで実装しているディレクトリ構造
functions/src/
├ index.ts // cloud functionsの関数として公開する
│
├ ui/ // 外部からの入力に対応するUI層
│ ├ console/ // consoleからの操作を実装
│ ├ liff/ // liffのためのAPIを実装
│ └ line/ // LINE Botからの操作を実装
│
├ application/ // 適切なdomainを呼び出し,処理を実行するapplication層
│ ├ usecase/ // ユースケースを実装
│ └ createMessage/ // FlexMessage作成する関数を実装(ui層の中にあるべきかも)
│
├ domain/ // domain層
│ ├ dataModel/ // システム内部で利用するモデル(型)
│ └ repository/ // application依存するためのリポジトリ抽象クラス
│
└ infrastructure/ // DBやAPIなど外部への接続を行うinfrastructure層
├ dataModel/ // 外部から受け取るデータのモデル(型)
├ repository/ // domainの抽象クラスの実装
└ service/ // 外部とのやりとりを実装
ui:システムを外部に公開するためのエントリポイントです.今回のシステムではLINE Botとのやりとりをするline,LIFFを使ったWebフォームで利用するliff,CLIからの処理を受け付ける(ダミーデータやリッチメニューの更新)ためのconsoleの3つに分かれています.ここは各エントリポイントで要求される指示に応じて適切なusecaseを呼び出します.また受け取ったデータをレスポンスする際に各UIに合わせて整形します.
application:usecaseの実装やそれに応じた適切なdomainの呼び出し,実行を担います.LINE BotのFlexMessage作成を作成するためのcreateMessageディテクトリもここに実装しています.エントリポイントがLINEの場合に必要とする部分なのでui/lineに実装しようと思ったのですが,domain/dataModelに依存する必要があるため,ここに実装しました.
domain:システム内で利用するdataModelそれ自体やそのデータの操作を定義するレイヤーです.dataModelにはモデルが,repositoryにはモデルを操作するためのリポジトリの抽象クラスが実装されています.リポジトリとはリポジトリパターンにおいてドメインモデルのアクセスを一手に引き受けるコンポーネントで,呼び出し側へアクセス先やその方法を隠蔽します.
infrastructure:外部(DBやAPI)へのアクセスや抽象リポジトリの具象化などを行うレイヤーです.dataModelには外部からのレスポンスのモデルを,repositoryにはdomain/repositoryのリポジトリの具象クラスを,serviceには外部のサービスにアクセスするクラスを実装しています.なお,domain/repositoryの抽象クラスと区別をつけるために,具象クラスには接尾辞にImplをつけています.
システム内の依存関係を図4に示します.
code:図 4 レイヤーの依存関係.mermaid
graph TD
UI --> App
App --> domain
infra --> domain
オニオンアーキテクチャの説明をした際に示した図1で外から中心に向かって依存するような形になりましたね.
さて,ここで勘のいい読者の方はこう思ったことでしょう「その依存関係でどうやってDBとかにアクセスしてるの?」と.図4に示したような依存関係だと外部にアクセスしているinfrastracture層の依存元がなく,外部にアクセスすることができません.しかし,infrastracture層に依存してしまっては外から内に向かって依存するというオニオンアーキテクチャの方針に反してしまいます.そんなジレンマを解決するのがDIという技術です.
DI
DIとはDependency Injectionの略で日本語訳すると「依存性注入」になります.危険な響きですが別にそんなことはありません,健全なものです.オブジェクト(今回の場合はリポジトリやサービス)をコードの実行時に注入することをDIという.domainで宣言したリポジトリの抽象クラスを,実行時にはinfrastructureの具象化したリポジトリを注入しています.DIを踏まえた実際の依存関係を図5に示します.なお,ここでDIするために用いたライブラリはtsyringeです.
code:図5 実際の依存関係.mermaid
graph TD
UI --> App
UI --> DI
App --> domain
App
infra --> domain
DI --> infra
DI --> domain
これで無事にDBなどにアクセスできるような流れになりました.結局外から内に依存するというルールは破られてしまっています.しかし,infrastructure層に外部へのアクセスを実装する以上,それに依存しないのは不可能です.その問題を解決するために,DIに「複雑になってしまう依存関係」という汚れ役を全て押し付けることで,それ以外の各レイヤーの依存関係はスッキリとしていて,オニオンアーキテクチャのルールを守りながらシステムを実現しています.
まとめ
以上で,LINE Bot開発におけるオニオンアーキテクチャの記事は終了です.本記事ではオニオンアーキテクチャやそれを用いたLINE Bot開発の大まかな説明,DIによる実現といった話をしました.アーキテクチャがミリしらだった人でもなんとなくの雰囲気を掴んでもらえていたら幸いです.